Skip to content

Experimental support for layered images #719

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 4, 2025
Merged

Conversation

melix
Copy link
Collaborator

@melix melix commented Apr 23, 2025

This commit introduces a Gradle DSL to support layered images creation. As of now, this is mainly aimed towards a single use case which is incremental builds. The test project demonstrates how to use the DSL to create a base layer which includes the JDK "java.base" module as well as all external dependencies used by the project.

The DSL builds on top of the binaries concept, by allowing them to declare either that they produce a layer, or that they use one or more layers. The DSL contains methods to make it easier to declare use or creation, as well as supports an easy way to declare that a layer should use only external dependencies. For example, this is how you would create a base layer and consume it from the main binary:

graalvmNative {
    binaries {
        libdependencies {
            createLayer {
                modules = ["java.base"]
                jars.from(externalDependenciesOf(configurations.runtimeClasspath))
            }
        }
        main {
            useLayer("libdependencies")
        }
    }
}

Note that there is a binary named libdependencies, and as soon as the createLayer is called, it will offer additional options which are specific to layers (for example declaring the list of packages or modules).

The DSL to create a layer contains the packages option, which could be used with automatic extraction of package names, which is why there is code to extract packages from jars, however, this code is currently unused for a reason: these packages can contain dependencies to "optional" modules, which cannot be figured out at build time. Typically, logback will support additional modules and load them dynamically, and if the package is included in the list and that the supporting dependencies are not on classpath, then the layer creation would fail. Therefore, the only reliable option right now is to use the jars property to set the list of jars which should belong to the layer.

@melix melix added the gradle-plugin Related to Gradle plugin label Apr 23, 2025
@melix melix requested a review from cstancu April 23, 2025 14:08
@melix melix self-assigned this Apr 23, 2025
@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Apr 23, 2025
@melix melix force-pushed the cc/layered-images branch from b1463f1 to 1ecb3a8 Compare May 5, 2025 09:08
@melix melix changed the title [WIP] Experimental support for layered images Experimental support for layered images May 7, 2025
@wirthi wirthi requested review from vjovanov and dnestoro May 7, 2025 13:18
.inheritIO()
.command("build/native/nativeCompile/layered-mn-app${IS_WINDOWS?".exe":""}")
def env = builder.environment()
env["LD_LIBRARY_PATH"] = testDirectory.resolve("build/native/nativeLibdependenciesCompile").toString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of setting LD_LIBRARY_PATH you can set -H:LinkerRPath={layer_path} when building the app layer to point to the directory containing the shared object(s) of underlying layer(s). The value gets passed to the linker as the -rpath. The path is relative to the directory where the executable resides. For example -H:LinkerRPath=$ORIGIN/lib points to the lib directory next to the executable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had tried this without success. That said I'm not sure it's a good idea: this is only used for running the app, it is not required when building. So if we change this to use -H:LinkerRPath, wouldn't that mean that the binary wouldn't be portable anymore if the base layer paths change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, if you set -rpath then that gets hardcoded in the binary, but it's common for apps to depend on dirs relative to the main binary. I remember you had issues with LD_LIBRARY_PATH before, that's why I mentioned it. Also, you need to always be sure to not overwrite LD_LIBRARY_PATH in case other apps depend on it.

main {
useLayer("libdependencies")
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add an alternative layered configuration where only the java.base is in the base layer, e.g., something like:

        libjavabase {
            createLayer {
                modules = ["java.base"]
            }
        }
        main {
            useLayer("libjavabase")
        }

That would be useful for testing when something goes wrong by adding all the dependencies in the base layer. Similarly for the Micronaut test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean, since the DSL already allows that. Or do you want to have an additional test? We try not to add too many redundant tests, since the build is already taking ages.

main {
useLayer("libdependencies")
}
}
Copy link
Member

@cstancu cstancu May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please also add a configuration that could switch off the layered build? In your early demo if I recall correctly you had an -incremental flag. It would be useful to be able to easily switch between these 3 scenarios for demo purposes (and maybe others in the future):

  • a regular build (no layers)
  • a layered build that has just java.base in the base layer
  • a layered build that has java.base and all dependencies in the base layer

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again it's unclear to me what you want. These are tests of the plugin itself, it doesn't quite make sense to add these options, since they wouldn't be used by our tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, then what would be the best way to showcase this plugin functionality to an end user?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe after we merge the plugin changes we can add a demo project to the Micronaut repo?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Although once this is merged and NBT released, I could implement this directly in the Micronaut Gradle plugins, where the options would make more sense, and the configuration could be hidden.

@cstancu cstancu self-requested a review May 20, 2025 14:48
cstancu
cstancu previously approved these changes May 20, 2025
Copy link
Member

@cstancu cstancu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks good to me with regards to the layer specific options/configs and the tests. @vjovanov @dnestoro please do a review of the plugin specific parts.

dnestoro
dnestoro previously approved these changes May 21, 2025
Copy link
Collaborator

@dnestoro dnestoro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I just left few suggestions

@@ -45,7 +45,7 @@ import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest

class JUnitFunctionalTests extends AbstractFunctionalTest {
def "test if JUint support works with various annotations, reflection and resources"() {

debug=true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a leftover?

melix added 3 commits June 3, 2025 19:13
This commit introduces a Gradle DSL to support layered images creation.
As of now, this is mainly aimed towards a single use case which is incremental builds.
The test project demonstrates how to use the DSL to create a base layer which includes
the JDK "java.base" module as well as all external dependencies used by the project.

The DSL builds on top of the binaries concept, by allowing them to declare either
that they produce a layer, or that they use one or more layers. The DSL contains
methods to make it easier to declare use or creation, as well as supports an easy
way to declare that a layer should use only external dependencies. For example,
this is how you would create a base layer and consume it from the main binary:

```
graalvmNative {
    binaries {
        libdependencies {
            createLayer {
                modules = ["java.base"]
                jars.from(externalDependenciesOf(configurations.runtimeClasspath))
            }
        }
        main {
            useLayer("libdependencies")
        }
    }
}
```

Note that there is a _binary_ named `libdependencies`, and as soon
as the `createLayer` is called, it will offer additional options which are
specific to layers (for example declaring the list of packages or modules).

The DSL to create a layer contains the `packages` option, which could be
used with automatic extraction of package names, which is why there is
code to extract packages from jars, however, this code is currently
unused for a reason: these packages can contain dependencies to "optional"
modules, which cannot be figured out at build time. Typically, logback
will support additional modules and load them dynamically, and if the
package is included in the list and that the supporting dependencies
are not on classpath, then the layer creation would fail. Therefore,
the only reliable option right now is to use the `jars` property to
set the list of jars which should belong to the layer.
Windows uses the argFile by default, which highlighted a bug: the
arg file was written in the output directory, which was cleaned
before running the task (cleaning is necessary or we'd have stale
files and up-to-date checking would be broken).

The arg file is now written in the temp directory instead.
@melix melix dismissed stale reviews from dnestoro and cstancu via d768f4b June 3, 2025 17:13
@melix melix force-pushed the cc/layered-images branch from 2b16729 to d768f4b Compare June 3, 2025 17:13
@dnestoro dnestoro merged commit a5cd92d into master Jun 4, 2025
317 checks passed
@dnestoro dnestoro deleted the cc/layered-images branch June 4, 2025 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
gradle-plugin Related to Gradle plugin OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants